Completed
Pull Request — master (#105)
by MusikAnimal
03:15
created

window.setupMonthYearChart   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
c 1
b 0
f 1
nc 1
dl 0
loc 8
rs 9.4285
nop 1
1
/**
2
 * Namespaces that have been excluded from view via namespace toggle table.
3
 * @type {Array}
4
 */
5
window.excludedNamespaces = [];
6
7
/**
8
 * Chart labels for the month/yearcount charts.
9
 * @type {Object} Keys are the chart IDs, values are arrays of strings.
10
 */
11
window.chartLabels = {};
12
13
/**
14
 * Number of digits of the max month/year total. We want to keep this consistent
15
 * for aesthetic reasons, even if the updated totals are fewer digits in size.
16
 * @type {Object} Keys are the chart IDs, values are integers.
17
 */
18
window.maxDigits = {};
19
20
$(function () {
21
    // Don't do anything if this isn't a Edit Counter page.
22
    if ($("body.ec").length === 0) {
23
        return;
24
    }
25
26
    // Set up charts.
27
    $('.chart-wrapper').each(function () {
28
        var chartType = $(this).data('chart-type');
29
        if ( chartType === undefined ) {
30
            return false;
31
        }
32
        var data = $(this).data('chart-data');
33
        var labels = $(this).data('chart-labels');
34
        var $ctx = $('canvas', $(this));
35
36
        /** global: Chart */
37
        new Chart($ctx, {
0 ignored issues
show
Unused Code Best Practice introduced by
The object created with new Chart($ctx, {Identif...e))))),false,false)))}) is not used but discarded. Consider invoking another function instead of a constructor if you are doing this purely for side effects.
Loading history...
38
            type: chartType,
39
            data: {
40
                labels: labels,
41
                datasets: [ { data: data } ]
42
            }
43
        });
44
45
        return undefined;
46
    });
47
48
    loadLatestGlobal();
49
50
    // Set up namespace toggle chart.
51
    setupToggleTable(window.namespaceTotals, window.namespaceChart, null, toggleNamespace);
52
});
53
54
/**
55
 * Callback for setupToggleTable(). This will show/hide a given namespace from
56
 * all charts, and update totals and percentages.
57
 * @param  {Object} newData New namespaces and totals, as returned by setupToggleTable.
58
 * @param  {String} key     Namespace ID of the toggled namespace.
59
 */
60
function toggleNamespace(newData, key)
61
{
62
    var total = 0, counts = [];
63
    Object.keys(newData).forEach(function (namespace) {
64
        var count = parseInt(newData[namespace], 10);
65
        counts.push(count);
66
        total += count;
67
    });
68
    var namespaceCount = Object.keys(newData).length;
69
70
    $('.namespaces--namespaces').text(
71
        namespaceCount.toLocaleString() + ' ' +
72
        $.i18n('num-namespaces', namespaceCount)
73
    );
74
    $('.namespaces--count').text(total.toLocaleString());
75
76
    // Now that we have the total, loop through once more time to update percentages.
77
    counts.forEach(function (count) {
78
        // Calculate percentage, rounded to tenths.
79
        var percentage = +(Math.round(
80
            ((count / total) * 100) + 'e+1'
81
        ) + 'e-1');
82
        // Update text with new value and percentage.
83
        $('.namespaces-table .sort-entry--count[data-value='+count+']').text(
84
            count.toLocaleString() + ' (' + percentage + '%)'
85
        );
86
    });
87
88
    // Loop through month and year charts, toggling the dataset for the newly excluded namespace.
89
    ['year', 'month'].forEach(function (id) {
90
        var chartObj = window[id + 'countsChart'],
91
            nsName = window.namespaces[key] || $.i18n('mainspace');
92
93
        // Figure out the index of the namespace we're toggling within this chart object.
94
        var datasetIndex;
95
        chartObj.data.datasets.forEach(function (dataset, i) {
96
            if (dataset.label === nsName) {
97
                datasetIndex = i;
98
            }
99
        });
100
101
        // Fetch the metadata and toggle the hidden property.
102
        var meta = chartObj.getDatasetMeta(datasetIndex);
103
        meta.hidden = meta.hidden === null ? !chartObj.data.datasets[datasetIndex].hidden : null;
104
105
        // Add this namespace to the list of excluded namespaces.
106
        if (meta.hidden) {
107
            window.excludedNamespaces.push(nsName);
108
        } else {
109
            window.excludedNamespaces = window.excludedNamespaces.filter(function (namespace) {
110
                return namespace !== nsName;
111
            });
112
        }
113
114
        // Update y-axis labels with the new totals.
115
        window[id + 'countsChart'].config.data.labels = getYAxisLabels(id, chartObj.data.datasets);
116
117
        // Refresh chart.
118
        chartObj.update();
119
    });
120
}
121
122
/**
123
 * Load recent global edits' HTML via AJAX, to not slow down the initial page load.
124
 * Only load if container is present, which is missing in subroutes, e.g. ec-namespacetotals, etc.
125
 */
126
function loadLatestGlobal()
127
{
128
    var $latestGlobalContainer = $("#latestglobal-container");
129
130
    if ($latestGlobalContainer[0]) {
131
        /** global: xtBaseUrl */
132
        var url = xtBaseUrl + 'ec-latestglobal/'
133
            + $latestGlobalContainer.data('project') + '/'
134
            + $latestGlobalContainer.data('username') + '?htmlonly=yes';
135
        $.ajax({
136
            url: url,
137
            timeout: 30000
138
        }).done(function (data) {
139
            $latestGlobalContainer.replaceWith(data);
140
        }).fail(function (_xhr, _status, message) {
141
            $latestGlobalContainer.replaceWith(
142
                $.i18n('api-error', 'Global contributions API: <code>' + message + '</code>')
143
            );
144
        });
145
    }
146
}
147
148
/**
149
 * Build the labels for the y-axis of the year/monthcount charts,
150
 * which include the year/month and the total number of edits across
151
 * all namespaces in that year/month.
152
 * @param {String} id ID prefix of the chart, either 'month' or 'year'.
153
 * @param {Array} datasets Datasets making up the chart.
154
 */
155
function getYAxisLabels(id, datasets)
156
{
157
    var labelsAndTotals = {};
158
    datasets.forEach(function (namespace) {
159
        if (window.excludedNamespaces.indexOf(namespace.label) !== -1) {
160
            return;
161
        }
162
163
        namespace.data.forEach(function (count, index) {
164
            if (!labelsAndTotals[window.chartLabels[id][index]]) {
165
                labelsAndTotals[window.chartLabels[id][index]] = 0;
166
            }
167
            labelsAndTotals[window.chartLabels[id][index]] += count;
168
        });
169
    });
170
171
    // Format labels with totals next to them. This is a bit hacky,
172
    // but it works! We use tabs (\t) to make the labels/totals
173
    // for each namespace line up perfectly.
174
    // The caveat is that we can't localize the numbers because
175
    // the commas are not monospaced :(
176
    return Object.keys(labelsAndTotals).map(function (year) {
177
        var digitCount = labelsAndTotals[year].toString().length;
178
        var numTabs = (window.maxDigits[id] - digitCount) * 2;
179
180
        // +5 for a bit of extra spacing.
181
        return year + Array(numTabs + 5).join("\t") +
182
            labelsAndTotals[year];
183
    });
184
}
185
186
/**
187
 * Set up the monthcounts or yearcounts chart. This is set on the window
188
 * because it is called in the yearcounts/monthcounts view.
189
 * @param {String} id 'year' or 'month'.
190
 * @param {Array} datasets Datasets grouped by mainspace.
191
 * @param {Array} labels The bare labels for the y-axis (years or months).
192
 * @param {Number} maxTotal Maximum value of year/month totals.
193
 */
194
window.setupMonthYearChart = function (id, datasets, labels, maxTotal) {
195
    /** @type {Array} Labels for each namespace. */
196
    var namespaces = datasets.map(function (dataset) {
197
        return dataset.label;
198
    });
199
200
    window.maxDigits[id] = maxTotal.toString().length
201
    window.chartLabels[id] = labels;
202
203
    window[id + 'countsChart'] = new Chart($('#' + id + 'counts-canvas'), {
204
        type: 'horizontalBar',
205
        data: {
206
            labels: getYAxisLabels(id, datasets),
207
            datasets: datasets
208
        },
209
        options: {
210
            tooltips: {
211
                intersect: true,
212
                callbacks: {
213
                    label: function (tooltip) {
214
                        return tooltip.xLabel.toLocaleString();
215
                    },
216
                    title: function (tooltip) {
217
                        var yLabel = tooltip[0].yLabel.replace(/\t.*/, '');
218
                        return yLabel + ' - ' + namespaces[tooltip[0].datasetIndex];
219
                    }
220
                }
221
            },
222
            responsive: true,
223
            maintainAspectRatio: false,
224
            scales: {
225
                xAxes: [{
226
                    stacked: true,
227
                    ticks: {
228
                        beginAtZero: true,
229
                        callback: function (value) {
230
                            if (Math.floor(value) === value) {
231
                                return value.toLocaleString();
232
                            }
233
                        }
234
                    }
235
                }],
236
                yAxes: [{
237
                    stacked: true,
238
                    barThickness: 18,
239
                }]
240
            },
241
            legend: {
242
                display: false
243
            }
244
        }
245
    });
246
}
247